#include "RedNeuronal.h"

float RedNeuronal::TOLERANCIA = 0.00002f;

inline double rando()
{
	static bool init = true;
	if (init)
	{
		init = false;
		srand(time(NULL));
	}
	return ((double) rand() / (RAND_MAX + 1.0));
}
/********************************NEURONA***********************************/
Neurona::Neurona(uint32_t inputs) :
		cant_inputs(inputs + 1)

{
	weight = (double*) malloc(sizeof(double) * (inputs + 1));
	//inicializo el vector de pesos, 1 peso mas que las entradas apra el Bias
	for (uint32_t i = 0; i < inputs + 1; ++i)
	{
		weight[i] = 2.0f * (rando() - 0.5);
	}
}

/************************************************************************/

/****************************CAPA_NEURONAL*******************************/

CapaNeuronal::CapaNeuronal(uint32_t cantNeuronas, uint32_t inputs/*,
 uint32_t outputs*/) :
		cant_neuronas(cantNeuronas)/*, cant_outputs(outputs)*/
{
	neuronas = (Neurona*) malloc(sizeof(Neurona) * cantNeuronas);
	for (uint32_t i = 0; i < cantNeuronas; ++i)
	{
		neuronas[i] = Neurona(inputs);
	}
}

/***********************************************************************/

/*****************************RED_NEURONAL******************************/
RedNeuronal::RedNeuronal(uint32_t cantIn, uint32_t cantOut)
{
	this->cant_inputs = cantIn;
	this->cant_outputs = cantOut;

}

/*
 * Agrega una capa neuronal a la red con una determinada cantidad de neuronas.
 * Los parametros de las neuronas se establecen automaticamente para mantener
 * las propiedades de la red.
 *
 *      @param cant_neuronas: cantidad de neuronas en la capa.
 */
void RedNeuronal::AgregarCapaNeuronal(
		uint32_t cant_neuronas/*, uint32_t cant_outputs*/)
{
	int cantIn = this->cant_inputs;

	if (this->capas.size())
	{
		this->capas.pop_back();
		cantIn = this->capas.at(this->capas.size() - 1).cant_neuronas;
	}

	this->capas.push_back(
			CapaNeuronal(cant_neuronas, cantIn/*, cant_outputs*/));

	this->capas.push_back(
			CapaNeuronal(cant_outputs, cant_neuronas/*, cant_outputs*/));
}

/*
 * Realiza la propagacion de la entrada a traves de la red.
 *
 *      @param in: vector de entrada a ser propagado por la red.
 *
 *      @return: vector con el resultado de la propacacion.
 */
vector<double> RedNeuronal::Pensar(vector<double> &in)
{
	vector<double> outputs;
	vector<double> inputs(in);
	if (in.size() != this->cant_inputs)
		return outputs;

	outputs = inputs;
	for (uint32_t i = 0; i < this->capas.size(); i++)
	{
		inputs = outputs;
		outputs.clear();

		uint32_t cantNeuronas = capas[i].cant_neuronas;
		for (uint32_t j = 0; j < cantNeuronas; j++)
		{
			double netinput = 0;

			uint32_t cantIn = capas[i].neuronas[j].cant_inputs;
			uint32_t k;
			for (k = 0; k < cantIn - 1; k++)
			{
				netinput += capas[i].neuronas[j].weight[k] * inputs[k];
			}

			//agregar Bias
			netinput += capas[i].neuronas[j].weight[k] * (-1);

			outputs.push_back(Sigmoid(netinput, 1));
		}
	}

	return outputs;
}

/*
 *Funcion sigmoid de la red
 *      @param input: valor
 *      @param amortiguamiento: amortiguamiento de la funcion
 *
 *      @return resultado de la aplicar la funcion al valor pasado por parametro.
 */
double RedNeuronal::Sigmoid(double input, double amortiguamiento)
{
	return (1 / (1 + exp(-input / amortiguamiento)));
}

double RedNeuronal::DerivativeSigmoid(double input, double amortiguamiento)
{
	double sig = Sigmoid(input, amortiguamiento);
	return sig * (1 - sig);
}

/*
 * ENTRENAMIENTO
 */

/*
 * Realiza lo mismo que Pensar() pero devuelve un vector de vectores con
 * TODOS LOS resultados intermedios de la propagacion incluido en
 * la posicion 0 el vector input.
 */
vector<vector<double> > RedNeuronal::GenerarOutput(vector<double> in)
{
	vector<double> input(in);
	vector<double> output;

	vector<vector<double> > outputs;
	outputs.push_back(in);

	output = input;
	for (uint32_t i = 0; i < this->capas.size(); i++)
	{
		uint32_t cantNeuronas = capas[i].cant_neuronas;
		input = output;
		output.clear();
		for (uint32_t j = 0; j < cantNeuronas; j++)
		{
			double netinput = 0;
			uint32_t cantIn = capas[i].neuronas[j].cant_inputs;
			uint32_t k;
			for (k = 0; k < cantIn - 1; k++)
			{
				netinput += capas[i].neuronas[j].weight[k] * input[k];
			}

			//agregar Bias
			netinput += capas[i].neuronas[j].weight[k] * (-1);
			double out = Sigmoid(netinput, 1);
			output.push_back(out);
		}
		outputs.push_back(output);
	}
	return outputs;

}

/*
 *Realiza el entrenamiento de la red dada una serie de entradas y targets correspondientes.
 *El entrenamiento se detiene una vez alcanzado el valor esperado (TOLERANCIA) o llegado
 *a un maximo de iteraciones (MAX_ITERACIONES)
 *      @param in: vector de vectores de inputs.
 *      @param tar: vector de vectores de targets.
 */
void RedNeuronal::Entrenar(vector<vector<double> > in,
		vector<vector<double> > tar)
{
	vector<double> input;
	vector<double> target;
	vector<vector<double> > outputs;
	vector<double> innerTarget;
	vector<bool> terminado(in.size(), false);
	uint32_t etapa = 0;

	if (in.size() != tar.size())
		return;

	while (etapa < MAX_ITERACIONES)
	{

		input = in.at(etapa % in.size());
		target = tar.at(etapa % tar.size());

		outputs = GenerarOutput(input);

		double errorCuadraticoMedio = 0;
		for (uint32_t i = 0; i < target.size(); i++)
		{
			errorCuadraticoMedio += 0.5f
					* (outputs.at(this->capas.size()).at(i) - target.at(i))
					* (outputs.at(this->capas.size()).at(i) - target.at(i));

		}
		if (errorCuadraticoMedio < RedNeuronal::TOLERANCIA)
			terminado.at(etapa % in.size()) = true;
		else
			terminado.at(etapa % in.size()) = false;

		for (uint32_t i = 0; i < terminado.size(); i++)
		{
			if (i == terminado.size() - 1)
				return;
			else if (!terminado.at(i))
				break;
		}

		for (uint32_t i = this->capas.size() - 1;
				i < this->capas.size() && i >= 0; i--)
		{
			uint32_t cantNeuronas = capas[i].cant_neuronas;
			innerTarget.clear();
			innerTarget = target;
			if (i > 0)
				target.resize(capas[i - 1].cant_neuronas, 0);
			else
				target.resize(this->cant_inputs, 0);

			for (uint32_t j = 0; j < cantNeuronas; j++)
			{
				double out = outputs.at(i + 1).at(j);
				double delta;
				if (i == this->capas.size() - 1)
				{
					delta = out * (1.0f - out) * (innerTarget.at(j) - out);
				}
				else
					delta = out * (1.0f - out) * (innerTarget.at(j));
				uint32_t cantIn = capas[i].neuronas[j].cant_inputs;
				for (uint32_t k = 0; k < cantIn - 1; k++)
				{
					target.at(k) += delta * capas[i].neuronas[j].weight[k];
					capas[i].neuronas[j].weight[k] += 0.5f * delta
							* outputs.at(i).at(k);
				}
			}
		}

		etapa++;
	}

	for (unsigned int i = 0; i < outputs.size() - 1; i++)
		outputs.at(i).clear();
	outputs.clear();
	innerTarget.clear();
	input.clear();
	target.clear();

}
